// Copyright (c) 2025 Shenzhen Kaihong Digital Industry Development Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{collections::HashSet, io::Write, path::{Path, PathBuf}, str::FromStr};

use log::{error, trace};
use walkdir::WalkDir;

use crate::{config::pkg_config::PkgConfigLoadError, database::{deploy_stat::{DeployStats, DeployStatsError}, local::pkg_stat_path, pkg_stat::{PkgStatFile, PkgStatFileError, StatSpec, StatSpecParseError}}, Arch};

use super::{fhs::{clear_fhs, FHS_USR_LINK_DIRS}, MergeRoot};

#[derive(Debug, thiserror::Error)]
pub enum UndeployError {
    #[error("failed to clear fhs env")]
    FhsClearFailed,
    #[error("failed to load deploy stats")]
    DeployStatsLoadError(#[from] DeployStatsError),
    #[error("failed to read pkg stat file '{1}'")]
    PkgStatReadError(#[source] PkgStatFileError, PathBuf),
    #[error("failed to remove file '{0}'")]
    RemoveFileError(PathBuf, #[source] std::io::Error),
    #[error("failed to load pkg config from spec '{1}'")]
    PkgConfigLoadError(#[source] PkgConfigLoadError, String),
    #[error("failed to parse pkg spec '{0}'")]
    StatSpecParseError(#[from] StatSpecParseError),
}

pub async fn undeploy_force_all<P: AsRef<Path>>(root: P) -> Result<(), UndeployError> {
    if !clear_fhs(root).await {
        return Err(UndeployError::FhsClearFailed);
    }
    Ok(())
}

pub async fn undeploy_pkgs<'a, I>(deploy_stats: &DeployStats, root: &Path, pkgs: I, arch: Arch) -> Result<(), UndeployError>
where I: Iterator<Item = &'a str>
{
    let mut remain_pkgs = deploy_stats.deployed_pkgs();
    for spec_str in pkgs {
        remain_pkgs.remove(spec_str);
    }

    let mut to_remove = WalkDir::new(root)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| !e.file_type().is_dir())
        .map(|e| e.into_path())
        .collect::<HashSet<_>>();

    for link in FHS_USR_LINK_DIRS {
        let path = root.join(link);
        if path.exists() {
            to_remove.remove(&path);
        }
    }

    for spec_str in remain_pkgs {
        let stat_path = pkg_stat_path(spec_str);
        let mut stat_file = PkgStatFile::open(&stat_path).await
            .map_err(|e| UndeployError::PkgStatReadError(e, stat_path.clone()))?;
        let deploy_config = stat_file.read_deploy_config().await
            .map_err(|e| UndeployError::PkgStatReadError(e, stat_path.clone()))?;
        let files = stat_file.read_files().await
            .map_err(|e| UndeployError::PkgStatReadError(e, stat_path.clone()))?;

        let stat_spec = StatSpec::from_str(spec_str)?;

        let merge_root = MergeRoot::new(deploy_config.as_ref(), stat_spec.arch, arch);

        for file in files {
            match &merge_root {
                MergeRoot::Normal => add_to_remove(&mut to_remove, &root.join(file), spec_str),
                MergeRoot::Host => add_to_remove(&mut to_remove, &root.join("host").join(file), spec_str),
                MergeRoot::Cross(arch) => add_to_remove(
                    &mut to_remove,
                    &root.join("cross").join(arch.to_string()).join(file),
                    spec_str,
                ),
                MergeRoot::HostNoIsolation(items) => {
                    if items.iter().any(|item| file.starts_with(item)) {
                        add_to_remove(&mut to_remove, &root.join(file), spec_str);
                    } else {
                        add_to_remove(&mut to_remove, &root.join("host").join(file), spec_str);
                    }
                },
            }
        }
    }

    {
        let mut dump = Vec::new();
        if dump_to_remove(&to_remove, &mut dump).is_ok() {
            trace!("{}", String::from_utf8_lossy(&dump));
        }
    }

    for path in to_remove {
        tokio::fs::remove_file(&path).await
            .map_err(|e| UndeployError::RemoveFileError(path.clone(), e))?;
    }

    Ok(())
}

fn add_to_remove(to_remove: &mut HashSet<PathBuf>, path: &Path, spec_str: &str) {
    if !path.is_symlink() && !path.exists() {
        error!("file '{}' of deployed pkg '{}' not found", path.display(), spec_str);
    }
    to_remove.remove(path);
}

fn dump_to_remove<W: Write>(to_remove: &HashSet<PathBuf>, mut w: W) -> Result<(), std::io::Error> {
    writeln!(w, "To remove files:")?;
    for path in to_remove {
        writeln!(w, "  - {}", path.display())?;
    }
    Ok(())
}
